﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using embPDFUtils;

using NLog;

using iTextSharp;
using iTextSharp;

using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;

using iTextSharp.text;
using iTextSharp.text.pdf;

namespace embPDFUtils
{
    using NLog.Config;

    /// <summary>
    /// Very flexible class, that enables
    /// merging documents from diffrent sources,
    /// via a simple XML-Configuration file.
    /// The merger is a XSD-Implemented business class.
    /// </summary>
    public class PdfDocumentMergerItextSharp : IPdfDocumentMerger
    {
        #region public memebers

        /// <summary>
        /// The tools used to split the document.
        /// </summary>
        private PdfDocumentToolsItextSharp _DocumentTools = new PdfDocumentToolsItextSharp();

        /// <summary>
        /// The logger for this class
        /// </summary>
        private static Logger logger = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// The merger used here.
        /// </summary>
        private documentmerger _Merger;

        #endregion

        #region public methods

        /// <summary>
        /// Gets the merger.
        /// </summary>
        public documentmerger Merger
        {
            get
            {
                return this._Merger;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PdfDocumentMergerItextSharp"/> class.
        /// </summary>
        public PdfDocumentMergerItextSharp()
        {
            _Merger = new documentmerger();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PdfDocumentMergerItextSharp"/> class.
        /// </summary>
        /// <param name="merger">The merger.</param>
        public PdfDocumentMergerItextSharp(documentmerger merger)
        {
            if (merger == null)
            {
                throw new ArgumentException("Parameter must not be null.", "merger");
            }

            this._Merger = merger;
        }

        /// <summary>
        /// Merges the documents.
        /// </summary>
        /// <param name="merger">The merger.</param>
        /// <param name="protocolFile">The protocol file.</param>
        public void MergeDocuments(documentmerger merger, string protocolFile = null)
        {
            if (merger == null)
            {
                //LOG
                logger.Log(LogLevel.Fatal, "An instance of documentmerger was null.");

                throw new ArgumentException("Argument must not be null.", "merger");
            }

            //LOG
            logger.Log(LogLevel.Info, "Starting merging process...");

            //The input directory for the source document
            string inputDirectory = string.Empty;
            //The output directory for the source document
            string outputDirectory = string.Empty;
            //The document id (regular file name)
            string documentId = string.Empty;
            //The type same, or save to new one
            string type = string.Empty;
            //Delete the old (input) document?
            bool deleteOld = false;
            //The pages to insert
            string pages = string.Empty;
            //The document name
            string docname = string.Empty;
            //Number of pages in destination document
            int numberOfPages = 0;
            //Number of pages in all source documents
            int numberOfPagesInSourceDocumentsCummulated = 0;
            //Total of pages for the current destination document
            int totalNumberOfPages = 0;
            //Current sourcedocuments
            ObservableCollection<documentmergerSourcedocument> sourceDocuments =
                new ObservableCollection<documentmergerSourcedocument>();

            //LOG
            logger.Log(LogLevel.Info, "Checking for source documents...");

            //Loop all destination documents
            foreach (documentmergerDestinationdocument currentDestinationDoc in merger.destinationdocuments)
            {
                //Initialize the most important parameters
                inputDirectory = currentDestinationDoc.inputdirectory;
                outputDirectory = currentDestinationDoc.outputdir;
                documentId = currentDestinationDoc.id;
                type = currentDestinationDoc.type;
                deleteOld = currentDestinationDoc.deleteold;
                docname = currentDestinationDoc.docname;

                //LOG
                logger.Log(
                    LogLevel.Info,
                    "Filtering all source documents for destination document {0}",
                    currentDestinationDoc.docname);

                //Get a list of all source documents, that fit to this current document

                #region Get all source documents for the current destination doc

                foreach (var currentInsert in merger.sourcedocuments)
                {
                    if (
                        currentDestinationDoc.inserts.Where(i => string.Equals(i.sourcedocumentid, currentInsert.id)).
                            FirstOrDefault() != null)
                    {
                        //Add the document, if it belongs to this destination document
                        sourceDocuments.Add(currentInsert);
                        //Get the number of all pages in all source documents for the current destination document
                        numberOfPagesInSourceDocumentsCummulated += currentInsert.pages.page.Count();
                    }
                }

                #endregion

                //To build up the path of the current InputDocument for merging
                string fullInputDocumentPath = string.Empty;
                //List of temporary file names for the document created
                var tempFileNames = new List<DocumentMergerHelper>();

                //LOG
                logger.Log(LogLevel.Info, "Creating Temporary Files from Source Files...");

                //Now, we have all source documents for this destination document, we can initiate the splitting
                //and merging actions

                #region Create the list of all temp files

                foreach (var currentInsert in currentDestinationDoc.inserts)
                {
                    pages = this.CreateTempPagesPdf(pages, tempFileNames, sourceDocuments, currentInsert);
                }

                #endregion

                #region Create Temp Merge Files

                //NOW, DO IT!EXCTRACT THE PAGES INTO TEMPORARY FILES TO BE MERGED IN
                foreach (var dohelper in tempFileNames)
                {
                    //Check, if we have images, or pure documents

                    if (dohelper.IsImageType)
                    {
                        Rectangle currentPageSize = PageSize.GetRectangle(currentDestinationDoc.pagesize.ToUpper());

                        if (currentPageSize == null)
                        {
                            throw new ArgumentNullException("currentPageSize");
                        }
                        else
                        {
                            //We have images here
                            _DocumentTools.CreatePdfFromMultipleImageFiles(
                                currentPageSize,
                                dohelper.TemporaryMergeDocumentPath,
                                dohelper.TemporaryMergeDocumentName,
                                dohelper.ImageFilePath,
                                "*.jpeg *.tiff *.gif *.png *.bmp *.jpg");
                            //Create a reader for the current merge file
                            dohelper.TempDocumentReader =
                                new PdfReader(
                                    Path.Combine(
                                        dohelper.TemporaryMergeDocumentPath, dohelper.TemporaryMergeDocumentName));
                            //LOG
                            logger.Log(
                                LogLevel.Info,
                                "Creating temporary PDF from image files {0}",
                                dohelper.TemporaryMergeDocumentName);
                        }
                    }
                    else
                    {
                        //We have a pdf file here
                        _DocumentTools.ExtractPdfPageRange(
                            dohelper.Pages,
                            dohelper.DocumentToMergeIntoPath,
                            dohelper.DocumentToMergeInto,
                            dohelper.TemporaryMergeDocumentPath,
                            dohelper.TemporaryMergeDocumentName);
                        //Create a reader for the current merge file
                        dohelper.TempDocumentReader =
                            new PdfReader(
                                Path.Combine(dohelper.TemporaryMergeDocumentPath, dohelper.TemporaryMergeDocumentName));
                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Creating temporary PDF from existing PDF file {0}",
                            dohelper.TemporaryMergeDocumentName);
                    }
                }

                #endregion

                #region Document Merging Process

                //All source documents for one destination document done,
                //Go on and merge everything now!
                //BASIC MERGING PROCESS

                //Sort the inserts ascending
                var orderedInserts = currentDestinationDoc.inserts.OrderBy(i => i.betweenstart);

                //Open destination document and get page count
                numberOfPages =
                    _DocumentTools.GetPdfPageCount(Path.Combine(currentDestinationDoc.inputdirectory, docname));

                //Size for merge document, later needed, not now
                totalNumberOfPages = numberOfPages + numberOfPagesInSourceDocumentsCummulated;

                //Create new pdf, with the current page size
                Document outputDoc = new Document(PageSize.GetRectangle(currentDestinationDoc.pagesize));

                //The final document
                PdfCopy finalDocument;

                //Check, if we need to create a new document, or if we should use the old one
                if (currentDestinationDoc.type.ToUpper().Equals("SAMEDOC"))
                {
                    //User the "same" document
                    //This means create the new one
                    //Rename the old to bla.pdf.old
                    //and rename the new to bla.pdf

                    //Create a PdfCopy writer
                    if (currentDestinationDoc.outputdir.ToUpper().Equals("SAME"))
                    {
                        finalDocument = new PdfCopy(
                            outputDoc,
                            new FileStream(
                                Path.Combine(
                                    currentDestinationDoc.inputdirectory, "NEW_" + currentDestinationDoc.docname),
                                FileMode.Create));
                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Creating final merge pdf {0} in {1}",
                            currentDestinationDoc.docname,
                            currentDestinationDoc.inputdirectory);
                    }
                    else
                    {
                        finalDocument = new PdfCopy(
                            outputDoc,
                            new FileStream(
                                Path.Combine(currentDestinationDoc.outputdir, "NEW_" + currentDestinationDoc.docname),
                                FileMode.Create));
                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Creating final merge pdf {0} in separate output dir {1}",
                            currentDestinationDoc.docname,
                            currentDestinationDoc.outputdir);
                    }
                }
                else
                {
                    //Use the new document
                    //name for the output file
                    //Create a PdfCopy writer

                    finalDocument = new PdfCopy(
                        outputDoc,
                        new FileStream(
                            Path.Combine(currentDestinationDoc.outputdir, currentDestinationDoc.newdocname),
                            FileMode.Create));
                    //LOG
                    logger.Log(
                        LogLevel.Info,
                        "Creating final merge pdf {0} in separate output dir, as new document {1}",
                        currentDestinationDoc.docname,
                        currentDestinationDoc.outputdir);
                }

                //Now open the Destination for merging, itself
                PdfReader destinationReader =
                    new PdfReader(Path.Combine(currentDestinationDoc.inputdirectory, currentDestinationDoc.docname));
                //Open the output doc
                outputDoc.Open();

                //The current insert
                documentmergerDestinationdocumentInsert insert;
                int insertPageCount = 0;
                //Loop through main file and add pages
                for (int i = 1; i <= numberOfPages; i++)
                {
                    //Check, if page numer is anywhere in 
                    //our insert range
                    if ((insert = orderedInserts.Where(f => f.betweenstart == i).FirstOrDefault()) != null)
                    {
                        //first write the current page of the current destination document
                        //So, that the page will not be overwritten
                        //Write the page to the finalDocument
                        finalDocument.AddPage(finalDocument.GetImportedPage(destinationReader, i));

                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Adding page {0} from current destination pdf {1}",
                            i,
                            Path.Combine(currentDestinationDoc.inputdirectory, currentDestinationDoc.docname));

                        //Insert all pages from the current tempfile
                        DocumentMergerHelper hlp =
                            tempFileNames.Where(t => t.Id.ToUpper().Equals(insert.sourcedocumentid.ToUpper())).
                                FirstOrDefault();

                        if (hlp != null)
                        {
                            insertPageCount = hlp.TempDocumentReader.NumberOfPages;

                            for (int y = 1; y <= insertPageCount; y++)
                            {
                                //Write the page to the finalDocument
                                finalDocument.AddPage(finalDocument.GetImportedPage(hlp.TempDocumentReader, y));

                                //LOG
                                logger.Log(
                                    LogLevel.Info,
                                    "Adding page {0} from temporary merge file {1}",
                                    y,
                                    Path.Combine(hlp.TemporaryMergeDocumentPath, hlp.TemporaryMergeDocumentName));
                            }

                            //Clean up
                            hlp.TempDocumentReader.Close();
                        }
                    }
                    else
                    {
                        //Write the page to the finalDocument
                        finalDocument.AddPage(finalDocument.GetImportedPage(destinationReader, i));
                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Adding page {0} from current destination pdf {1}",
                            i,
                            Path.Combine(currentDestinationDoc.inputdirectory, currentDestinationDoc.docname));
                    }
                }

                //Cleanup
                outputDoc.Close();
                destinationReader.Close();

                //Delete the tempfiles
                this.DeleteTempFiles(tempFileNames);

                this.CheckDeleteOriginalInputFile(currentDestinationDoc, deleteOld);

                #endregion
            } //END LOOP DESTINATION DOCUMENTS
        }

        #endregion

        #region private methods

        /// <summary>
        /// Checks the delete original input file.
        /// </summary>
        /// <param name="currentDestinationDoc">The current destination doc.</param>
        /// <param name="deleteOld">if set to <c>true</c> [delete old].</param>
        private void CheckDeleteOriginalInputFile(
            documentmergerDestinationdocument currentDestinationDoc, bool deleteOld)
        {
            if (deleteOld)
            {
                if (File.Exists(Path.Combine(currentDestinationDoc.inputdirectory, currentDestinationDoc.docname)))
                {
                    //Delete the old file, and rename the new one
                    File.Delete(Path.Combine(currentDestinationDoc.inputdirectory, currentDestinationDoc.docname));

                    //Rename the new one
                    File.Move(
                        Path.Combine(currentDestinationDoc.inputdirectory, "NEW_" + currentDestinationDoc.docname),
                        Path.Combine(currentDestinationDoc.inputdirectory, currentDestinationDoc.docname));
                }
            }
        }

        /// <summary>
        /// Deletes the temp files.
        /// </summary>
        /// <param name="tempFileNames">The temp file names.</param>
        private void DeleteTempFiles(List<DocumentMergerHelper> tempFileNames)
        {
            foreach (var tempfile in tempFileNames)
            {
                if (File.Exists(Path.Combine(tempfile.TemporaryMergeDocumentPath, tempfile.TemporaryMergeDocumentName)))
                {
                    try
                    {
                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Deleting temporary intputfile {0} ",
                            Path.Combine(tempfile.TemporaryMergeDocumentPath, tempfile.TemporaryMergeDocumentName));

                        File.Delete(
                            Path.Combine(
                                Path.Combine(tempfile.TemporaryMergeDocumentPath, tempfile.TemporaryMergeDocumentName)));

                        //LOG
                        logger.Log(
                            LogLevel.Info,
                            "Deleted temporary intputfile {0} ",
                            Path.Combine(tempfile.TemporaryMergeDocumentPath, tempfile.TemporaryMergeDocumentName));
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                }
            }
        }

        #endregion

        /// <summary>
        /// Creates the temp pages PDF.
        /// </summary>
        /// <param name="pages">The pages.</param>
        /// <param name="tempFileNames">The temp file names.</param>
        /// <param name="sourceDocuments">The source documents.</param>
        /// <param name="currentInsert">The current insert.</param>
        /// <returns></returns>
        private string CreateTempPagesPdf(
            string pages,
            List<DocumentMergerHelper> tempFileNames,
            ObservableCollection<documentmergerSourcedocument> sourceDocuments,
            documentmergerDestinationdocumentInsert currentInsert)
        {
            //Where to insert the current destination document
            var betweenStart = (int)currentInsert.betweenstart;
            var betweenEnd = (int)currentInsert.betweenend;

            //Get the next source document, that belongs to this destinationdocument,
            //identified by id
            documentmergerSourcedocument doc =
                sourceDocuments.Where(i => i.id == currentInsert.sourcedocumentid).FirstOrDefault();

            if (doc == null)
            {
                return pages;
            }

            //Get the pages to extract
            foreach (documentmergerSourcedocumentPagesPage p in doc.pages.page)
            {
                pages += p.number.ToString() + ",";
            }

            //remove the last comma
            pages = pages.Substring(0, pages.Length - 1);

            //Now, save the pages as temporary document
            tempFileNames.Add(
                new DocumentMergerHelper
                    {
                        DocumentToMergeInto = doc.name,
                        DocumentToMergeIntoPath = doc.filepath,
                        TemporaryMergeDocumentPath = doc.filepath,
                        TemporaryMergeDocumentName = Path.GetFileName(Path.GetTempFileName()),
                        Pages = pages,
                        IsImageType = doc.type.Equals("image"),
                        ImageFilePath = doc.filepath,
                        Id = doc.id
                    });

            //reset counters etc.
            pages = string.Empty;
            return pages;
        }

        #region static methods

        /// <summary>
        /// Gets the prop value.
        /// </summary>
        /// <param name="src">The SRC.</param>
        /// <param name="propName">Name of the prop.</param>
        /// <returns></returns>
        public static object GetPropValue(object src, string propName)
        {
            return src.GetType().GetProperty(propName).GetValue(src, null);
        }

        #endregion
    }

    #region Internal Classes

    /// <summary>
    /// Help to save all 
    /// the interesting stuff about
    /// the source documents, and
    /// the resulting temporary pdf documents
    /// </summary>
    internal class DocumentMergerHelper
    {
        /// <summary>
        /// Gets or sets the temporary merge document path.
        /// </summary>
        /// <value>
        /// The temporary merge document path.
        /// </value>
        public string TemporaryMergeDocumentPath { get; set; }

        /// <summary>
        /// Gets or sets the name of the temporary merge document.
        /// </summary>
        /// <value>
        /// The name of the temporary merge document.
        /// </value>
        public string TemporaryMergeDocumentName { get; set; }

        /// <summary>
        /// Gets or sets the document to merge into.
        /// </summary>
        /// <value>
        /// The document to merge into.
        /// </value>
        public string DocumentToMergeInto { get; set; }

        /// <summary>
        /// Gets or sets the document to merge into path.
        /// </summary>
        /// <value>
        /// The document to merge into path.
        /// </value>
        public string DocumentToMergeIntoPath { get; set; }

        /// <summary>
        /// Gets or sets the pages.
        /// </summary>
        /// <value>
        /// The pages.
        /// </value>
        public string Pages { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is image type.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is image type; otherwise, <c>false</c>.
        /// </value>
        public bool IsImageType { get; set; }

        /// <summary>
        /// Gets or sets the image file path.
        /// </summary>
        /// <value>
        /// The image file path.
        /// </value>
        public string ImageFilePath { get; set; }

        /// <summary>
        /// Gets or sets the id.
        /// </summary>
        /// <value>
        /// The id.
        /// </value>
        public string Id { get; set; }

        /// <summary>
        /// Gets or sets the temp document reader.
        /// </summary>
        /// <value>
        /// The temp document reader.
        /// </value>
        public PdfReader TempDocumentReader { get; set; }
    }

    #endregion
}